// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

// 转到校验和数据库查找

// 开始：建造！cmd_go_引导
// +建造！cmd_go_引导

package modfetch

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"cmd/go/internal/base"
	"cmd/go/internal/cfg"
	"cmd/go/internal/lockedfile"
	"cmd/go/internal/web"

	"golang.org/x/mod/module"
	"golang.org/x/mod/sumdb"
	"golang.org/x/mod/sumdb/note"
)

// useSumDB报告是否为给定模块使用Go校验和数据库。
func useSumDB(mod module.Version) bool {
	return cfg.GOSUMDB != "off" && !module.MatchPrefixPatterns(cfg.GONOSUMDB, mod.Path)
}

// lookupSumDB返回给定模块的Go校验和数据库的Go.sum行，
// 以及数据库的名称。
func lookupSumDB(mod module.Version) (dbname string, lines []string, err error) {
	dbOnce.Do(func() {
		dbName, db, dbErr = dbDial()
	})
	if dbErr != nil {
		return "", nil, dbErr
	}
	lines, err = db.Lookup(mod.Path, mod.Version)
	return dbName, lines, err
}

var (
	dbOnce sync.Once
	dbName string
	db     *sumdb.Client
	dbErr  error
)

func dbDial() (dbName string, db *sumdb.Client, err error) {
	// $GOSUMDB可以是“密钥”或“密钥url”，
	// 并且该密钥可以是完全验证器密钥
	// 或者我们已知密钥列表中的主机。

	// 特例：sum.golang.google.cn
	// 是一个别名，可在中国大陆访问，
	// 对于sum.golang.org。如果还有更多
	// 其中，我们应该添加一个类似knownGOSUMDB的地图。
	gosumdb := cfg.GOSUMDB
	if gosumdb == "sum.golang.google.cn" {
		gosumdb = "sum.golang.org https:// sum.golang.google.cn“
	}

	key := strings.Fields(gosumdb)
	if len(key) >= 1 {
		if k := knownGOSUMDB[key[0]]; k != "" {
			key[0] = k
		}
	}
	if len(key) == 0 {
		return "", nil, fmt.Errorf("missing GOSUMDB")
	}
	if len(key) > 2 {
		return "", nil, fmt.Errorf("invalid GOSUMDB: too many fields")
	}
	vkey, err := note.NewVerifier(key[0])
	if err != nil {
		return "", nil, fmt.Errorf("invalid GOSUMDB: %v", err)
	}
	name := vkey.Name()

	// 数据库名称中没有有趣的事情。
	direct, err := url.Parse("https:// “+姓名）
	if err != nil || strings.HasSuffix(name, "/") || *direct != (url.URL{Scheme: "https", Host: direct.Host, Path: direct.Path, RawPath: direct.RawPath}) || direct.RawPath != "" || direct.Host == "" {
		return "", nil, fmt.Errorf("invalid sumdb name (must be host[/path]): %s %+v", name, *direct)
	}

	// 确定如何访问数据库。
	var base *url.URL
	if len(key) >= 2 {
		// 使用$GOSUMDB中列出的显式备用URL，
		// 绕过默认URL派生和任何代理。
		u, err := url.Parse(key[1])
		if err != nil {
			return "", nil, fmt.Errorf("invalid GOSUMDB URL: %v", err)
		}
		base = u
	}

	return name, sumdb.NewClient(&dbClient{key: key[0], name: name, direct: direct, base: base}), nil
}

type dbClient struct {
	key    string
	name   string
	direct *url.URL

	once    sync.Once
	base    *url.URL
	baseErr error
}

func (c *dbClient) ReadRemote(path string) ([]byte, error) {
	c.once.Do(c.initBase)
	if c.baseErr != nil {
		return nil, c.baseErr
	}

	var data []byte
	start := time.Now()
	targ := web.Join(c.base, path)
	data, err := web.GetBytes(targ)
	if false {
		fmt.Fprintf(os.Stderr, "%.3fs %s\n", time.Since(start).Seconds(), targ.Redacted())
	}
	return data, err
}

// initBase确定连接到数据库的基本URL。
// 确定URL需要向代理发送网络流量，
// 所以这项工作被推迟到我们需要从
// 数据库。如果我们需要的一切都在本地缓存中
// c、 ReadRemote永远不会被调用，我们永远不会做这项工作。
func (c *dbClient) initBase() {
	if c.base != nil {
		return
	}

	// 依次尝试代理，直到找到如何连接到此数据库为止。
	// None
	// 在使用代理访问任何校验和数据库URL之前，代理
	// 客户端应首先获取受支持的<proxyURL>/sumdb/<sumdb name>。
	// None
	// 如果该请求返回成功的（HTTP 200）响应，则代理
	// 支持代理校验和数据库请求。在这种情况下，客户
	// 应仅使用代理访问方法，不得退回到直接访问
	// 连接到数据库。
	// None
	// 如果/sumdb/<sumdb name>/supported检查失败并显示“未找到”（HTTP
	// 404）或“消失”（HTTP 410）响应，或者如果代理配置为下降
	// 返回错误时，客户端将尝试下一个代理。如果没有
	// 离开代理，或者如果代理为“直接”或“关闭”，则客户端应
	// 直接连接到该数据库。
	// None
	// 任何其他响应都被视为数据库不可用。
	// None
	// 请参阅https:
	err := TryProxies(func(proxy string) error {
		switch proxy {
		case "noproxy":
			return errUseProxy
		case "direct", "off":
			return errProxyOff
		default:
			proxyURL, err := url.Parse(proxy)
			if err != nil {
				return err
			}
			if _, err := web.GetBytes(web.Join(proxyURL, "sumdb/"+c.name+"/supported")); err != nil {
				return err
			}
			// 成功这个代理将帮助我们。
			c.base = web.Join(proxyURL, "sumdb/"+c.name)
			return nil
		}
	})
	if errors.Is(err, fs.ErrNotExist) {
		// 没有代理，或所有代理都失败（404、410或被允许）
		// 或者我们达成了明确的“直接”或“关闭”。
		c.base = c.direct
	} else if err != nil {
		c.baseErr = err
	}
}

// ReadConfig从c.key读取密钥
// 否则从GOPATH/pkg/sumdb/<file>读取配置（最新的树头）。
func (c *dbClient) ReadConfig(file string) (data []byte, err error) {
	if file == "key" {
		return []byte(c.key), nil
	}

	if cfg.SumdbDir == "" {
		return nil, errors.New("could not locate sumdb file: missing $GOPATH")
	}
	targ := filepath.Join(cfg.SumdbDir, file)
	data, err = lockedfile.Read(targ)
	if errors.Is(err, fs.ErrNotExist) {
		// 将不存在的文件视为空，以引导“最新”文件
		// 第一次连接到给定数据库时。
		return []byte{}, nil
	}
	return data, err
}

// WriteConfig重写最新的树头。
func (*dbClient) WriteConfig(file string, old, new []byte) error {
	if file == "key" {
		// 不应该发生。
		return fmt.Errorf("cannot write key")
	}
	if cfg.SumdbDir == "" {
		return errors.New("could not locate sumdb file: missing $GOPATH")
	}
	targ := filepath.Join(cfg.SumdbDir, file)
	os.MkdirAll(filepath.Dir(targ), 0777)
	f, err := lockedfile.Edit(targ)
	if err != nil {
		return err
	}
	defer f.Close()
	data, err := io.ReadAll(f)
	if err != nil {
		return err
	}
	if len(data) > 0 && !bytes.Equal(data, old) {
		return sumdb.ErrWriteConflict
	}
	if _, err := f.Seek(0, 0); err != nil {
		return err
	}
	if err := f.Truncate(0); err != nil {
		return err
	}
	if _, err := f.Write(new); err != nil {
		return err
	}
	return f.Close()
}

// ReadCache从中读取缓存的查找或磁贴
// GOPATH/pkg/mod/cache/download/sumdb，
// 将被“go clean-modcache”删除。
func (*dbClient) ReadCache(file string) ([]byte, error) {
	targ := filepath.Join(cfg.GOMODCACHE, "cache/download/sumdb", file)
	data, err := lockedfile.Read(targ)
	// lockedfile.Write不会自动创建包含内容的文件。
	// 在创建文件和锁定文件进行写入之间有一段时间，
	// 在此期间，可以锁定空文件进行读取。
	// 将观察空文件视为未找到文件。
	if err == nil && len(data) == 0 {
		err = &fs.PathError{Op: "read", Path: targ, Err: fs.ErrNotExist}
	}
	return data, err
}

// WriteCache更新缓存的查找或磁贴。
func (*dbClient) WriteCache(file string, data []byte) {
	targ := filepath.Join(cfg.GOMODCACHE, "cache/download/sumdb", file)
	os.MkdirAll(filepath.Dir(targ), 0777)
	lockedfile.Write(targ, bytes.NewReader(data), 0666)
}

func (*dbClient) Log(msg string) {
	// 现在没有
}

func (*dbClient) SecurityError(msg string) {
	base.Fatalf("%s", msg)
}
